/*
 *  Copyright 2009 Jamie Gennis <jgennis@gmail.com>.
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 * 
 *       http://www.apache.org/licenses/LICENSE-2.0
 * 
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *  under the License.
 */

package com.google.code.gwt.remoteaction.group.client;

import java.util.HashSet;
import java.util.Set;

import com.google.code.gwt.remoteaction.client.AbstractActionProxy;
import com.google.code.gwt.remoteaction.client.Action;
import com.google.code.gwt.remoteaction.client.ActionFilter;
import com.google.code.gwt.remoteaction.client.ActionResult;
import com.google.code.gwt.remoteaction.group.client.ActionGroup.Factory;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.inject.Inject;

/**
 * An action filter that groups multiple action invocations together and calls a
 * single group-completion {@link AsyncCallback} once all actions have
 * successfully completed or one of the actions fails.
 * 
 * @author jgennis
 */
public class GroupingActionFilter extends AbstractActionProxy implements ActionFilter,
    ActionGrouper {

  final ActionGroup.Factory actionGroupFactory;

  final Set<ActionGroup> activeActionGroups = new HashSet<ActionGroup>();

  /**
   * Construct a new {@code GroupingActionFilter}.
   */
  public GroupingActionFilter() {
    this.actionGroupFactory = new ActionGroup.Factory();
  }

  /**
   * Construct a new {@code GroupingActionFilter}.
   * 
   * @param actionGroupFactory the factory that will be used to create new
   *          {@link ActionGroup} instances
   */
  @Inject
  public GroupingActionFilter(Factory actionGroupFactory) {
    this.actionGroupFactory = actionGroupFactory;
  }

  /**
   * @see com.google.code.gwt.remoteaction.client.ActionExecutor#execute(com.google.code.gwt.remoteaction.client.Action,
   *      com.google.gwt.user.client.rpc.AsyncCallback)
   */
  @Override
  public void execute(Action action, final AsyncCallback<ActionResult> callback) {
    if (activeActionGroups.isEmpty()) {
      proxyExecution(action, callback);
    } else {
      final Set<ActionGroup> actionGroups = new HashSet<ActionGroup>(activeActionGroups);
      for (ActionGroup actionGroup : actionGroups)
        actionGroup.addAction();
      proxyExecution(action, new AsyncCallback<ActionResult>() {

        @Override
        public void onSuccess(final ActionResult result) {
          /*
           * Enqueue a deferred command to execute each of the action group's
           * onSuccess() method. We use a deferred command here so that if one
           * of the onSuccess() callbacks throws an unchecked exception that
           * won't prevent the other callbacks from being called.
           */
          for (final ActionGroup actionGroup : actionGroups) {
            addDeferredCommand(new Command() {

              @Override
              public void execute() {
                Throwable thrown = result.getException();
                if (thrown == null)
                  actionGroup.onSuccess();
                else
                  actionGroup.onFailure(thrown);
              }
            });
          }
          /*
           * Clear the action groups for this callback so that if the callback
           * is called multiple times only the first time counts towards the
           * action groups.
           */
          actionGroups.clear();
          // Call the original callback
          callback.onSuccess(result);
        }

        @Override
        public void onFailure(final Throwable caught) {

          /*
           * Enqueue a deferred command to execute each of the action group's
           * onFailure() method. We use a deferred command here so that if one
           * of the onFailure() callbacks throws an unchecked exception that
           * won't prevent the other callbacks from being called.
           */
          for (final ActionGroup actionGroup : actionGroups) {
            addDeferredCommand(new Command() {

              @Override
              public void execute() {
                actionGroup.onFailure(caught);
              }
            });
          }
          /*
           * Clear the action groups for this callback so that if the callback
           * is called multiple times only the first time counts towards the
           * action groups.
           */
          actionGroups.clear();
          // Call the original callback
          callback.onFailure(caught);
        }
      });
    }
  }

  /**
   * @see com.google.code.gwt.remoteaction.group.client.ActionGrouper#beginActionGroup
   *      (com.google.gwt.user.client.rpc.AsyncCallback)
   */
  @Override
  public ActionGroup beginActionGroup(AsyncCallback<Void> callback) {
    if (callback == null)
      throw new IllegalArgumentException("The 'callback' argument may not be null");
    ActionGroup actionGroup = actionGroupFactory.create(callback);
    activeActionGroups.add(actionGroup);
    return actionGroup;
  }

  /**
   * @see com.google.code.gwt.remoteaction.group.client.ActionGrouper#endActionGroup
   *      (com.google.code.gwt.remoteaction.group.client.ActionGroup)
   */
  @Override
  public void endActionGroup(ActionGroup actionGroup) {
    if (actionGroup == null || !activeActionGroups.remove(actionGroup)) {
      throw new IllegalArgumentException("The 'actionGroup' argument is not an active action "
          + "group for this GroupingActionFilter instance");
    }
    actionGroup.endActionGroup();
  }

  /**
   * Add a deferred command that will be executed once all events have been
   * handled.
   * 
   * This method exists so that it can be mocked for unit testing. It simply
   * calls {@link DeferredCommand#addCommand(Command)}.
   * 
   * @param command the command to execute
   */
  void addDeferredCommand(Command command) {
    DeferredCommand.addCommand(command);
  }
}
